package com.naojump.android.picasa;

import static com.naojump.util.NUtils.hasChar;
import static com.naojump.util.NUtils.lconv;

import android.content.Intent;
import android.util.Log;

import com.naojump.android.gdata.client.NClientLogin;
import com.naojump.android.gdata.util.NAuthenticationException;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

/**
 * <strong>Android implementation</strong> for<br>
 * Picasa Web Albums Data API<br>
 * 
 * @author Naoto Murata
 * @since 1
 * @version 8
 * @see "http://code.google.com/apis/picasaweb/docs/2.0/developers_guide_protocol.html"
 */
public class NPicasaWebAlbums {

    /** DEBUG */
    private static final boolean DEBUG = false;

    /** TAG */
    private static final String TAG = NPicasaWebAlbums.class.getName();

    /** GData-Version (2) */
    public static final String VERSION = "2";

    /** Base URL of Picasa Web Albums Data API */
    public static final String BASE_URL = "http://picasaweb.google.com/data/";

    /** Home URL of Picasa Web Albums */
    public static final String HOME_URL = "http://picasaweb.google.com/home";

    /**
     * Name of the Google service: Picasa Web Albums Data API ("lh2")
     * 
     * @see "http://code.google.com/apis/gdata/faq.html#clientlogin"
     * */
    public static final String SERVICE_NAME = "lh2";

    /** Unmodifiable list of supported mime types */
    public static List<String> SUPPORTED_MIME_TYPES = new ArrayList<String>();
    static {
        SUPPORTED_MIME_TYPES.add("image/jpeg");
        SUPPORTED_MIME_TYPES.add("image/gif");
        SUPPORTED_MIME_TYPES.add("image/bmp");
        SUPPORTED_MIME_TYPES.add("image/tiff");
        SUPPORTED_MIME_TYPES.add("image/png");
        SUPPORTED_MIME_TYPES.add("image/x-image-raw");
        SUPPORTED_MIME_TYPES.add("application/photoshop");
        SUPPORTED_MIME_TYPES.add("video/3gpp");
        SUPPORTED_MIME_TYPES.add("video/avi");
        SUPPORTED_MIME_TYPES.add("video/quicktime");
        SUPPORTED_MIME_TYPES.add("video/mp4");
        SUPPORTED_MIME_TYPES.add("video/mpeg");
        SUPPORTED_MIME_TYPES.add("video/mpeg4");
        SUPPORTED_MIME_TYPES.add("video/msvideo");
        SUPPORTED_MIME_TYPES.add("video/x-ms-asf");
        SUPPORTED_MIME_TYPES.add("video/x-ms-wmv");
        SUPPORTED_MIME_TYPES.add("video/x-msvideo");

        SUPPORTED_MIME_TYPES = Collections
                .unmodifiableList(SUPPORTED_MIME_TYPES);
    }

    /** Unmodifiable list of supported mime types */
    public static List<String[]> MIME_TYPE_EXTENSIONS = new ArrayList<String[]>();
    static {
        MIME_TYPE_EXTENSIONS.add(new String[] { "image/jpeg", ".jpg", ".jpeg" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "image/gif", ".gif" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "image/bmp", ".bmp" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "image/tiff", ".tiff", ".tif" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "image/png", ".png" });
        // MIME_TYPE_EXTENSIONS.add(new String[] {"image/x-image-raw", ".raw"});
        MIME_TYPE_EXTENSIONS.add(new String[] { "application/photoshop", ".psd" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "video/3gpp", ".3gp", ".3gpp" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "video/avi", ".avi" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "video/quicktime", ".qt" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "video/mp4", ".mp4" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "video/mpeg", ".mpg", ".mpeg" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "video/mpeg4", ".mp4" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "video/msvideo", ".avi" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "video/x-ms-asf", ".asf" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "video/x-ms-wmv", ".wmv" });
        MIME_TYPE_EXTENSIONS.add(new String[] { "video/x-msvideo", ".avi" });

        MIME_TYPE_EXTENSIONS = Collections
                .unmodifiableList(MIME_TYPE_EXTENSIONS);
    }

    /** Default user id ("default") */
    public static final String USER_ID_DEFAULT = "default";

    /** Access: private */
    public static final String ACCESS_PRIVATE = "private";

    /** Access: public */
    public static final String ACCESS_PUBLIC = "public";

    /** Single instance of DocumentBuilder, read-only */
    private static DocumentBuilder sBldr = null;

    /**
     * Single instance of DocumentBuilder, read-only
     * 
     * @return Single instance of DocumentBuilder
     */
    private static DocumentBuilder getDocumentBuilder() {
        if (DEBUG)
            Log.d(TAG, "getDocumentBuilder");

        if (sBldr == null) {
            try {
                sBldr = DocumentBuilderFactory.newInstance()
                        .newDocumentBuilder();
            } catch (ParserConfigurationException e) {
                if (DEBUG)
                    Log.e(TAG, "Uncontrollable.", e);

                System.exit(1);
            }
        }
        return sBldr;
    }

    /**
     * Check whether your media's type supported on Picasa or not.
     * 
     * @param mimeType
     *            mime type
     * @return true if supported, false if unsupported.
     */
    public static boolean isSupportedMineType(String mimeType) {
        if (DEBUG)
            Log.d(TAG, "isSupportedMineType");

        return SUPPORTED_MIME_TYPES.contains(mimeType);
    }

    /** User ID on Picasa Web Albums */
    private String mUserId;

    /**
     * Authentication status
     * 
     * @see com.naojump.android.gdata.client.NClientLogin.Status
     */
    private NClientLogin.Status mStatus;

    /**
     * Http request method
     */
    private HttpRequestBase mRequest = null;

    /**
     * Constructor
     * 
     * @param userId
     *            userId (recommended)
     * @param status
     *            Authentication status (optional)
     */
    public NPicasaWebAlbums(String userId, NClientLogin.Status status) {
        mUserId = userId;
        mStatus = status;
    }

    /**
     * Abort post.
     * 
     * @return true if aborted, false if not posting.
     */
    public boolean abortPost() {
        boolean ret = false;
        synchronized (this) {
            if (mRequest != null) {
                mRequest.abort();
                mRequest = null;
                ret = true;
            }
        }
        return ret;
    }

    /**
     * Creates new public web album.
     * 
     * @return HttpStatus
     * @throws NAuthenticationException
     *             Throws when not authenticated.
     * @throws IOException
     *             Throws when network error has occurred.
     */
    public HttpResponse createNewPublicAlbum() throws NAuthenticationException,
            IOException {
        if (DEBUG)
            Log.d(TAG, "createNewPublicAlbum");

        requireAuth();

        String content = ""
                + "<entry xmlns='http://www.w3.org/2005/Atom'"
                + "    xmlns:media='http://search.yahoo.com/mrss/'"
                + "    xmlns:gphoto='http://schemas.google.com/photos/2007'>"
                + "  <title type='text'>Public Album</title>"
                + "  <summary type='text'></summary>"
                + "  <gphoto:location></gphoto:location>"
                + "  <gphoto:access>public</gphoto:access>"
                + "  <gphoto:timestamp></gphoto:timestamp>"
                + "  <media:group>"
                + "    <media:keywords></media:keywords>"
                + "  </media:group>"
                + "  <category scheme='http://schemas.google.com/g/2005#kind'"
                + "    term='http://schemas.google.com/photos/2007#album'></category>"
                + "</entry>";

        HttpClient client = new DefaultHttpClient();
        HttpPost post = new HttpPost(BASE_URL + "feed/api/user/" + mUserId);
        StringEntity body = new StringEntity(content);

        // Authentication info
        if (mStatus != null && hasChar(mStatus.getAuth())) {
            post.addHeader("Authorization", "GoogleLogin auth="
                    + mStatus.getAuth());
        }

        // Version
        post.addHeader("GData-Version", VERSION);

        // Body
        body.setContentType("application/atom+xml");
        post.setEntity(body);

        HttpResponse res = client.execute(post);
        return res;
    }

    /**
     * Gets a list of YOUR albums (using mUserId field).
     * 
     * @param access
     *            Visibility value
     * @return List of &lt;entry&gt; elements
     * @throws NAuthenticationException
     *             Throws when not authenticated.
     * @throws IOException
     *             Throws when network error has occurred.
     * @see "http://code.google.com/intl/ja/apis/picasaweb/docs/2.0/developers_guide_protocol.html#ListAlbums"
     */
    public List<Node> getAlbums(String access) throws NAuthenticationException,
            IOException {

        return getAlbums(mUserId, access);
    }

    /**
     * Gets a list of albums.
     * 
     * @param userId
     *            userId
     * @param access
     *            Visibility value
     * @return List of &lt;entry&gt; elements
     * @throws NAuthenticationException
     *             Throws when not authenticated.
     * @throws IOException
     *             Throws when network error has occurred.
     * @see "http://code.google.com/intl/ja/apis/picasaweb/docs/2.0/developers_guide_protocol.html#ListAlbums"
     */
    public List<Node> getAlbums(String userId, String access)
            throws NAuthenticationException, IOException {
        if (DEBUG)
            Log.d(TAG, "getAlbums");

        Document doc = getDocument("feed/api/user/" + userId + "?access="
                + access);
        if (doc != null) {
            return lconv(doc.getElementsByTagName("entry"));
        } else {
            return null;
        }
    }

    /**
     * Retrieves XML Document from Picasa Web Albums Data API.
     * 
     * @param cmd
     *            command
     * @return XML DOM Document, or null probably if invalid command is
     *         specified.
     * @throws NAuthenticationException
     *             Throws when not authenticated.
     * @throws IOException
     *             Throws when network error has occurred.
     */
    private Document getDocument(String cmd) throws NAuthenticationException,
            IOException {
        if (DEBUG)
            Log.d(TAG, "getDocument");

        HttpClient client = new DefaultHttpClient();
        HttpGet req = new HttpGet(BASE_URL + cmd);

        // Authentication info
        if (mStatus != null && hasChar(mStatus.getAuth())) {
            req.addHeader("Authorization", "GoogleLogin auth="
                    + mStatus.getAuth());
        }

        // Version
        req.addHeader("GData-Version", VERSION);

        // Execute.
        HttpResponse res = client.execute(req);

        switch (res.getStatusLine().getStatusCode()) {
        case HttpStatus.SC_OK: {
            try {
                return getDocumentBuilder().parse(res.getEntity().getContent());
            } catch (SAXException e) {
                if (DEBUG)
                    Log.e(TAG, "Uncontrollable.", e);
                throw new RuntimeException("Uncontrollable.", e);
            }
        }
        case HttpStatus.SC_UNAUTHORIZED:
        case HttpStatus.SC_FORBIDDEN: {
            throw new NAuthenticationException(res.getStatusLine()
                    .getReasonPhrase());
        }
        default: {
            if (DEBUG)
                Log.e(TAG, "Uncontrollable.");

            throw new RuntimeException("Uncontrollable. ("
                    + res.getStatusLine().getStatusCode() + " "
                    + res.getStatusLine().getReasonPhrase() + ")");
        }
        }
    }

    /**
     * Post to Picasa Web Albums Data API.<br>
     * (Unfortunately I support some images only now.)<br>
     * 
     * @param cmd
     *            command
     * @param media
     *            Intent of the media
     * @param in
     *            InputStream of the media
     * @param length
     *            Length of the media
     * @param name
     *            Name of the media (optional)
     * @return HttpResponse
     * @throws IOException
     *             Throws when network error has occurred.
     */
    private HttpResponse post(String cmd, Intent media, InputStream in,
            long length, String name) throws IOException {
        if (DEBUG)
            Log.d(TAG, "post");

        HttpClient client = new DefaultHttpClient();
        HttpPost post = new HttpPost(BASE_URL + cmd);

        // Fixed at version 5.
        // If length is specified, upload content directly.
        // Otherwise, count content length and upload.
        // XXX memory management implementation...
        AbstractHttpEntity body = null;
        if (length >= 1) {
            body = new InputStreamEntity(in, length);
        } else {
            ByteArrayOutputStream bytes = new ByteArrayOutputStream(256 * 1024);
            byte[] buf = new byte[16 * 1024];
            int len;
            while ((len = in.read(buf, 0, buf.length)) >= 1) {
                bytes.write(buf, 0, len);
            }
            body = new ByteArrayEntity(bytes.toByteArray());
        }

        // Authentication info
        if (mStatus != null && hasChar(mStatus.getAuth())) {
            post.addHeader("Authorization", "GoogleLogin auth="
                    + mStatus.getAuth());
        }

        // Version
        post.addHeader("GData-Version", VERSION);

        // Media info
        post.addHeader("Content-Type", media.getType());
        if (hasChar(name)) {
            post.addHeader("Slug", name);
        }

        // Body
        body.setChunked(true);
        post.setEntity(body);

        synchronized (this) {
            mRequest = post;
        }
        HttpResponse res = null;
        try {
            res = client.execute(post);
        } catch (IOException e) {
            boolean isNotAborted = false;
            synchronized (this) {
                if (mRequest != null && (!mRequest.isAborted())) {
                    isNotAborted = true;
                }
            }
            if (isNotAborted) {
                throw e;
            }
        }
        synchronized (this) {
            mRequest = null;
        }
        return res;
    }

    /**
     * Post a media to Picasa Web Albums Data API.<br>
     * (Unfortunately I support some images only now.)<br>
     * 
     * @param albumId
     *            albumId
     * @param media
     *            Intent of the media
     * @param in
     *            InputStream of the media
     * @param length
     *            Length of the media
     * @param name
     *            Name of the media (optional)
     * @return HttpResponse
     * @throws NAuthenticationException
     *             Throws when not authenticated.
     * @throws NUnsupportedMediaTypeException
     *             Throws if the media type of user-selected one unsupported on
     *             Picasa.
     * @throws IOException
     *             Throws when network error has occurred.
     */
    public HttpResponse postMedia(String albumId, Intent media, InputStream in,
            long length, String name) throws NAuthenticationException,
            NUnsupportedMediaTypeException, IOException {
        if (DEBUG)
            Log.d(TAG, "postMedia");

        requireAuth();

        if (!isSupportedMineType(media.getType())) {
            throw new NUnsupportedMediaTypeException(media.getType());
        }

        HttpResponse res = null;
        if (!hasChar(mUserId)) {
            res = post("feed/api/user/default/albumid/default", media, in,
                    length, name);
        } else {
            res = post("feed/api/user/" + mUserId + "/albumid/" + albumId,
                    media, in, length, name);
        }

        return res;
    }

    /**
     * Checks whether authenticated or not.<br>
     * If successfully authenticated, this method does nothing.<br>
     * 
     * @throws NAuthenticationException
     *             Throws when not authenticated.
     */
    private void requireAuth() throws NAuthenticationException {
        if (DEBUG)
            Log.d(TAG, "requireAuth");

        if ((mStatus == null) || (!hasChar(mStatus.getAuth()))) {
            throw new NAuthenticationException();
        }
    }

}
